package algorithms;

import infoPackage.*;
import java.util.ArrayList;
import java.util.Random;

public class GeneticAlgorithm {

    public boolean elitism = false;
    public boolean multipleCrossover = false;
    public int elitismNum = 2; //Selects the best n chromosomes - MUST BE EVEN
    Random r = new Random();
    public float ps = 0;
    public float pc = 0;
    public float pm = 0;
    public ArrayList<Chromosome> old = null; //Copy of the array chromosomes.
    public ArrayList<Chromosome> chromosomes = null;
    public ArrayList<River> rivers = null;
    public ArrayList<Section> sections = null;
    float eval = 0;

    /*
     * Creates the initial population and calculates
     * the adaptation function for each chromosome.
     */
    public GeneticAlgorithm(int chromoSize, ArrayList<River> rivers, ArrayList<Section> sections, int nChromo,
            boolean elitism, int elitismNum, boolean multipleCrossover, int maxDam, float minDistance) {
        this.rivers = rivers;
        this.sections = sections;
        this.elitism = elitism;
        if (elitism) {
            this.elitismNum = elitismNum;
        }
        this.multipleCrossover = multipleCrossover;
        chromosomes = new ArrayList<Chromosome>();
        for (int i = 0; i < nChromo; i++) {
            Chromosome c = new Chromosome(chromoSize, maxDam, minDistance);
            c.adaptationFunction(sections);
            chromosomes.add(c);
        }
    }

    private void calculateEval(ArrayList<Chromosome> c) {
        eval = 0;
        for (int i = 0; i < c.size(); i++) {
            eval += c.get(i).getValue();
        }
        if (eval < 0) {
            eval = 0;
        }
    }

    /**
     * Calls all phase functions to create a new generation of chromosomes.
     */
    public void nextGeneration() {
        old = (ArrayList<Chromosome>) chromosomes.clone();

        chromosomes.clear();
        if (chromosomes.isEmpty()) {

            Pair[] pairs = selection();

            if (multipleCrossover) {
                multipleCrossover(pairs);
            } else {
                simpleCrossover(pairs);
            }

            mutation();

            for (int i = 0; i < chromosomes.size(); i++) {
                System.out.println("\n      ********* Adaptation Value of C" + i + " *********\n");
                chromosomes.get(i).adaptationFunction(this.sections);
                chromosomes.get(i).setSelected(false);
            }
        }
    }

    ;

    /**
     * Selection phase - Calls elitism() if elitism is being used
     * @return pairs - an array of indexes from chromossomes to pass to the crossover phase
     */
    private Pair[] selection() {
        System.out.println("\n      ********* Selection *********\n");
        int repeated = 0;

        this.ps = r.nextFloat();
        System.out.println("         Selection probability: " + this.ps + "\n");

        calculateEval(old);
        System.out.println("         Evaluation value: " + eval + "\n");

        for (int i = 0; i < old.size(); i++) {
            calculatePS(old.get(i));
        }

        if (elitism) {
            elitism(old, chromosomes, this.ps);
        }

        int[] roulette = new int[100];	//criacao de uma roulette com 100 posicoes. Ocupada pelos cromossomas consoante o seu valor de ps.

        int index = 0;
        for (int j = 0; j < old.size(); j++) {
            int n = (int) (old.get(j).ps * 100); //number of elements to insert in the roulette
            if (index < roulette.length && n > 0) {
                index = insert(roulette, n, j, index);
            }
        }

        int length = (old.size() - chromosomes.size()) / 2;

        Pair[] pairs = new Pair[length];

        for (int i = 0; i < pairs.length; i++) {
            pairs[i] = new Pair();
        }

        for (int i = 0; i < pairs.length; i++) {
            int a = getRandomInt(roulette.length);
            int b = getRandomInt(roulette.length);
            Pair aux = new Pair(roulette[a], roulette[b]);
            for (int j = 0; j < pairs.length; j++) {
                if (!pairs[j].equals(aux)) {
                    pairs[i] = aux;
                } else {
                    repeated++;
                }
            }
            int done = pairs.length - repeated;
            for (int z = done; z < pairs.length; z++) {
                int c = getBestPS(this.ps, old);
                int d = getBestPS(this.ps, old);
                if (c == d) {
                    z--;
                } else {
                    pairs[z] = new Pair(c, d);
                }
            }
        }

        System.out.print("         Selected pairs to crossover: ");
        for (int i = 0; i < pairs.length; i++) {

            if (pairs[i] != null) {
                pairs[i].printPair();
            }
        }
        System.out.println();
        return pairs;
    }

    private void stopGeneration(Chromosome chromosome, int i) {
        System.out.println("**** This Chromosome has a evaluation greater than 90% ****\n");
        System.out.println("Chromo " + i + " | Value: " + chromosome.getValue());
        chromosome.printChromosome();
        System.exit(2);
    }

    /**
     *
     * @param roulette array of probability
     * @param n times to insert the value on the array
     * @param value to insert on the array
     * @param index to start putting the values
     * @return index of last inserted value
     */
    private int insert(int[] roulette, int n, int value, int index) {
        int i = 0;
        for (; i < n; i++) {
            roulette[index + i] = value;
        }
        return (index + i);
    }

    /**
     * Elitism phase - it happens only if boolean elitism is true.
     *
     * */
    private void elitism(ArrayList<Chromosome> oldChromo, ArrayList<Chromosome> newChromo, float ps) {
        System.out.print("         Elitism choosen pairs: ");
        while (newChromo.size() < elitismNum) {
            int index = getBestPS(ps, oldChromo);
            if (index >= 0) {
                System.out.print(index + ", ");
                newChromo.add(oldChromo.get(index));
            }
        }
        System.out.println("\n");
    }

    /**
     * Crossover phase
     */
    private void simpleCrossover(Pair[] pairs) {
        System.out.println("\n      ********* S_Crossover *********\n");

        try {
            for (int i = 0; i < pairs.length; i++) {
                Chromosome old_1 = old.get(pairs[i].pair[0]);
                Chromosome old_2 = old.get(pairs[i].pair[1]);

                int crossover = getRandomInt(old_1.chromosome.size());

                if (crossover == 0) {
                    crossover = getRandomInt(old_1.chromosome.size());
                }

                Chromosome new_1 = new Chromosome();
                Chromosome new_2 = new Chromosome();

                System.out.println("         Pair " + i + ": cross on " + crossover + "\n");
                for (int j = 0; j < crossover; j++) {
                    new_1.chromosome.add(old_1.chromosome.get(j));
                    new_2.chromosome.add(old_2.chromosome.get(j));
                }

                for (int z = crossover; z < old_1.chromosome.size(); z++) {
                    new_1.chromosome.add(old_2.chromosome.get(z));
                    new_2.chromosome.add(old_1.chromosome.get(z));
                }

                chromosomes.add(new_1);
                chromosomes.add(new_2);
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();
            System.out.println("exception: GET i= " + "size: " + old.size());
            simpleCrossover(pairs);
            return;
        }
    }

    private void multipleCrossover(Pair[] pairs) {
        System.out.println("\n      ********* M_Crossover *********\n");

        try {
            for (int i = 0; i < pairs.length; i++) {
                Chromosome old_1 = old.get(pairs[i].pair[0]);
                Chromosome old_2 = old.get(pairs[i].pair[1]);

                int times = getRandomInt(old_1.chromosome.size());

                System.out.println("         Pair " + i + " will cross  " + times + " times");

                Chromosome new_1 = new Chromosome();
                Chromosome new_2 = new Chromosome();

                //Copy entire old chromosomes into new chromosomes
                for (int j = 0; j < old_1.chromosome.size(); j++) {
                    new_1.chromosome.add(old_1.chromosome.get(j));
                    new_2.chromosome.add(old_2.chromosome.get(j));
                }

                for (int z = 0; z < times; z++) {
                    int a = 0, b = 0;
                    while (a == b) {
                        a = getRandomInt(old_1.chromosome.size());
                        b = getRandomInt(old_1.chromosome.size());
                    }

                    // ensure a range begining at 'a' and ending at 'b' [a,b]
                    if (a > b) {
                        int x = b;
                        b = a;
                        a = x;
                    }
                    System.out.println("            From " + a + " to " + b);

                    for (int j = a; j < b; j++) {
                        new_1.chromosome.remove(j);
                        new_1.chromosome.add(j, old_1.chromosome.get(j));
                        new_2.chromosome.remove(j);
                        new_2.chromosome.add(j, old_2.chromosome.get(j));
                    }

                    for (int j = b; j < old_1.chromosome.size(); j++) {
                        new_1.chromosome.remove(j);
                        new_1.chromosome.add(j, old_2.chromosome.get(j));
                        new_2.chromosome.remove(j);
                        new_2.chromosome.add(j, old_1.chromosome.get(j));
                    }
                }

                chromosomes.add(new_1);
                chromosomes.add(new_2);
                System.out.println();
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();
            System.out.println("exception: GET i= " + "size: " + old.size());
            simpleCrossover(pairs);
            return;
        }
    }

    /**
     * Mutation phase
     */
    private void mutation() {
        System.out.println("\n      ********* Mutation *********\n");
        int times = getRandomInt(chromosomes.size());
        System.out.println("         Mutation times: " + times);
        for (; times > 0; times--) {
            int option = getRandomInt(6);
            int c_index = getRandomInt(chromosomes.size());
            int d_index = getRandomInt(chromosomes.get(c_index).chromosome.size());
            System.out.println("            At the " + c_index + " chromo in the " + d_index + " dam. Option: " + option);
            switch (option) {
                case 0:
                    chromosomes.get(c_index).chromosome.get(d_index).mutateConstruct();
                    break;
                case 1:
                    chromosomes.get(c_index).chromosome.get(d_index).mutateCapacity();
                    break;
                case 2:
                    chromosomes.get(c_index).chromosome.get(d_index).mutateVolume();
                    break;
                case 3:
                    chromosomes.get(c_index).chromosome.get(d_index).mutateNatureImpact();
                    break;
                case 4:
                    chromosomes.get(c_index).chromosome.get(d_index).mutateMaterials();
                    break;
                case 5:
                    chromosomes.get(c_index).chromosome.get(d_index).mutateTecnology();
                    break;
                default:
                    break;
            }
        }

    }

    /**
     * @return integer from zero to size
     */
    public int getRandomInt(int size) {
        return ((int) (r.nextFloat() * size));
    }

    /**
     * @return the index of the chromosome with the best value
     * adaption function.
     */
    public int getBestValue(ArrayList<Chromosome> chromo) {
        float best = Integer.MIN_VALUE;
        int index = -1;
        for (int i = 0; i < chromo.size(); i++) {
            if ((chromo.get(i).getValue() > best) && !(chromo.get(i).isSelected())) {
                if (index > -1) {
                    chromo.get(index).setSelected(false);
                }
                chromo.get(i).setSelected(true);
                best = chromo.get(i).getValue();
                index = i;
            }
        }
        return index;
    }

    /**
     */
    private void calculatePS(Chromosome c) {
        c.ps = c.getValue() / eval;
        //System.out.println("c.getValue = " + c.getValue() + "  eval= " + eval + elitismNum + "  c.ps = " + c.ps);
    }

    public int getBestPS(float prob, ArrayList<Chromosome> c) {
        float best = Integer.MIN_VALUE;
        int index = -1;
        for (int i = 0; i < c.size(); i++) {
            float ps = c.get(i).ps;
            if ((ps <= prob) && (ps > best) && !(c.get(i).isSelected())) {
                if (index > -1) {
                    c.get(index).setSelected(false);
                }
                c.get(i).setSelected(true);
                best = c.get(i).ps;
                index = i;
            }
        }
        if (index > -1) {
            return index;
        }
        return getRandomInt(c.size());
    }

    private void setAllSelected(ArrayList<Chromosome> c, boolean selected) {
        for (int i = 0; i < c.size(); i++) {
            c.get(i).setSelected(selected);
        }
    }

    private void setAllAdaptionValue() {
        for (int i = 0; i < chromosomes.size(); i++) {
            chromosomes.get(i).adaptationFunction(this.sections);
        }
    }

    public void printPopulation(ArrayList<Chromosome> c) {
        for (int i = 0; i < c.size(); i++) {
            System.out.println("Chromo " + i + "\n   |Value: " + c.get(i).getValue() + "\n   |PS: " + c.get(i).ps);
            //c.get(i).printChromosome();
            //System.out.println();
        }
    }
}
